// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package hcso

import (
	"fmt"

	"yunion.io/x/jsonutils"
	"yunion.io/x/pkg/errors"

	api "yunion.io/x/cloudmux/pkg/apis/compute"
	"yunion.io/x/cloudmux/pkg/cloudprovider"
	"yunion.io/x/cloudmux/pkg/multicloud"
	"yunion.io/x/cloudmux/pkg/multicloud/huawei"
)

// date: 2019.07.15
// In Huawei cloud, there are only two routing tables in a vpc, which are
// self-defined routing tables and peer-to-peer routing tables.
// The routing in these two tables is different, one's NextHop is a IP address and
// the other one's NextHop address is a instance ID of peer-to-peer connection.
// The former has no id and it's Type is ROUTE_TYPR_IP, and the latter's Type is ROUTE_TYPE_PEER.

const (
	ROUTE_TYPR_IP   = "IP"
	ROUTE_TYPE_PEER = "peering"
)

type SRouteEntry struct {
	multicloud.SResourceBase
	huawei.HuaweiTags
	routeTable *SRouteTable

	ID          string // route ID
	Type        string // route type
	Destination string // route destination
	NextHop     string // route next hop (ip or id)
}

func (route *SRouteEntry) GetId() string {
	if len(route.ID) == 0 {
		return route.Destination + ":" + route.NextHop
	}
	return route.ID
}

func (route *SRouteEntry) GetName() string {
	return ""
}

func (route *SRouteEntry) GetGlobalId() string {
	return route.GetId()
}

func (route *SRouteEntry) GetStatus() string {
	return api.ROUTE_ENTRY_STATUS_AVAILIABLE
}

func (route *SRouteEntry) Refresh() error {
	return nil
}

func (route *SRouteEntry) IsEmulated() bool {
	return false
}

func (route *SRouteEntry) GetType() string {
	if route.Type == ROUTE_TYPE_PEER {
		return api.ROUTE_ENTRY_TYPE_CUSTOM
	}
	return api.ROUTE_ENTRY_TYPE_SYSTEM
}

func (route *SRouteEntry) GetCidr() string {
	return route.Destination
}

func (route *SRouteEntry) GetNextHopType() string {
	// In Huawei Cloud, NextHopType is same with itself
	switch route.Type {
	case ROUTE_TYPE_PEER:
		return api.NEXT_HOP_TYPE_VPCPEERING
	default:
		return ""
	}
}

func (route *SRouteEntry) GetNextHop() string {
	return route.NextHop
}

// SRouteTable has no ID and Name because there is no id or name of route table in huawei cloud.
// And some method such as GetId and GetName of ICloudRouteTable has no practical meaning
type SRouteTable struct {
	multicloud.SResourceBase
	huawei.HuaweiTags
	region *SRegion
	vpc    *SVpc

	VpcId       string
	Description string
	Type        string
	Routes      []*SRouteEntry
}

func NewSRouteTable(vpc *SVpc, Type string) SRouteTable {
	return SRouteTable{
		region: vpc.region,
		vpc:    vpc,
		Type:   Type,
		VpcId:  vpc.GetId(),
	}

}

func (self *SRouteTable) GetId() string {
	return self.GetGlobalId()
}

func (self *SRouteTable) GetName() string {
	return ""
}

func (self *SRouteTable) GetGlobalId() string {
	return fmt.Sprintf("%s-%s", self.GetVpcId(), self.GetType())
}

func (self *SRouteTable) GetStatus() string {
	return api.ROUTE_TABLE_AVAILABLE
}

func (self *SRouteTable) Refresh() error {
	return nil
}

func (self *SRouteTable) IsEmulated() bool {
	return false
}

func (self *SRouteTable) GetDescription() string {
	return self.Description
}

func (self *SRouteTable) GetRegionId() string {
	return self.region.GetId()
}

func (self *SRouteTable) GetVpcId() string {
	return self.VpcId
}

func (self *SRouteTable) GetType() cloudprovider.RouteTableType {
	return cloudprovider.RouteTableTypeSystem
}

func (self *SRouteTable) GetIRoutes() ([]cloudprovider.ICloudRoute, error) {
	if self.Routes == nil {
		err := self.fetchRoutes()
		if err != nil {
			return nil, err
		}
	}
	ret := []cloudprovider.ICloudRoute{}
	for i := range self.Routes {
		ret = append(ret, self.Routes[i])
	}
	return ret, nil
}

// fetchRoutes fetch Routes
func (self *SRouteTable) fetchRoutes() error {
	if self.Type == ROUTE_TYPR_IP {
		return self.fetchRoutesForIP()
	}
	return self.fetchRoutesForPeer()
}

// fetchRoutesForIP fetch the Routes which Type is ROUTE_TYPR_IP through vpc's get api
func (self *SRouteTable) fetchRoutesForIP() error {
	ret, err := self.region.ecsClient.Vpcs.Get(self.GetVpcId(), map[string]string{})
	if err != nil {
		return errors.Wrap(err, "get vpc info error")
	}
	routeArray, err := ret.GetArray("routes")
	routes := make([]*SRouteEntry, 0, len(routeArray))
	for i := range routeArray {
		destination, err := routeArray[i].GetString("destination")
		if err != nil {
			return errors.Wrap(err, "get destination of route error")
		}
		nextHop, err := routeArray[i].GetString("nexthop")
		if err != nil {
			return errors.Wrap(err, "get nexthop of route error")
		}
		routes = append(routes, &SRouteEntry{
			routeTable:  self,
			ID:          "",
			Type:        ROUTE_TYPR_IP,
			Destination: destination,
			NextHop:     nextHop,
		})
	}
	self.Routes = routes
	return nil
}

// fetchRoutesForPeer fetch the routes which Type is ROUTE_TYPE_PEER through vpcRoute's list api
func (self *SRouteTable) fetchRoutesForPeer() error {
	retPeer, err := self.region.ecsClient.VpcRoutes.List(map[string]string{"vpc_id": self.GetVpcId()})
	if err != nil {
		return errors.Wrap(err, "get peer route error")
	}
	routesPeer := make([]*SRouteEntry, 0, retPeer.Total)
	for i := range retPeer.Data {
		route := retPeer.Data[i]
		id, err := route.GetString("id")
		if err != nil {
			return errors.Wrap(err, "get id of peer route error")
		}
		destination, err := route.GetString("destination")
		if err != nil {
			return errors.Wrap(err, "get destination of peer route error")
		}
		nextHop, err := route.GetString("nexthop")
		if err != nil {
			return errors.Wrap(err, "get nexthop of peer route error")
		}
		routesPeer = append(routesPeer, &SRouteEntry{
			routeTable:  self,
			ID:          id,
			Type:        ROUTE_TYPE_PEER,
			Destination: destination,
			NextHop:     nextHop,
		})
	}
	self.Routes = routesPeer
	return nil
}

func (self *SRouteTable) GetAssociations() []cloudprovider.RouteTableAssociation {
	result := []cloudprovider.RouteTableAssociation{}
	return result
}

func (self *SRouteTable) CreateRoute(route cloudprovider.RouteSet) error {
	if route.NextHopType != api.NEXT_HOP_TYPE_VPCPEERING {
		return cloudprovider.ErrNotSupported
	}
	err := self.region.CreatePeeringRoute(self.vpc.GetId(), route.Destination, route.NextHop)
	if err != nil {
		return errors.Wrapf(err, " self.region.CreatePeeringRoute(%s,%s,%s)", self.vpc.GetId(), route.Destination, route.NextHop)
	}
	return nil
}

func (self *SRouteTable) UpdateRoute(route cloudprovider.RouteSet) error {
	err := self.RemoveRoute(route)
	if err != nil {
		return errors.Wrap(err, "self.RemoveRoute(route)")
	}
	err = self.CreateRoute(route)
	if err != nil {
		return errors.Wrap(err, "self.CreateRoute(route)")
	}
	return nil
}

func (self *SRouteTable) RemoveRoute(route cloudprovider.RouteSet) error {
	err := self.region.DeletePeeringRoute(route.RouteId)
	if err != nil {
		return errors.Wrapf(err, "self.region.DeletePeeringRoute(%s)", route.RouteId)
	}
	return nil
}

// GetRouteTables return []SRouteTable of self
func (self *SVpc) getRouteTables() ([]SRouteTable, error) {
	// every Vpc has two route table in Huawei Cloud
	routeTableIp := NewSRouteTable(self, ROUTE_TYPR_IP)
	routeTablePeer := NewSRouteTable(self, ROUTE_TYPE_PEER)
	if err := routeTableIp.fetchRoutesForIP(); err != nil {
		return nil, errors.Wrap(err, `get route table whilc type is "ip" error`)
	}
	if err := routeTablePeer.fetchRoutesForPeer(); err != nil {
		return nil, errors.Wrap(err, `get route table whilc type is "peering" error`)
	}
	ret := make([]SRouteTable, 0, 2)
	if len(routeTableIp.Routes) != 0 {
		ret = append(ret, routeTableIp)
	}
	if len(routeTablePeer.Routes) != 0 {
		ret = append(ret, routeTablePeer)
	}
	return ret, nil
}

// GetRouteTables return []SRouteTable of vpc which id is vpcId if vpcId is no-nil,
// otherwise return []SRouteTable of all vpc in this SRegion
func (self *SRegion) GetRouteTables(vpcId string) ([]SRouteTable, error) {
	vpcs, err := self.GetVpcs()
	if err != nil {
		return nil, errors.Wrap(err, "Get Vpcs error")
	}
	if vpcId != "" {
		for i := range vpcs {
			if vpcs[i].GetId() == vpcId {
				vpcs = vpcs[i : i+1]
				break
			}
		}
	}
	ret := make([]SRouteTable, 0, 2*len(vpcs))
	for _, vpc := range vpcs {
		routetables, err := vpc.getRouteTables()
		if err != nil {
			return nil, errors.Wrapf(err, "get vpc's route tables whilch id is %s error", vpc.GetId())
		}
		ret = append(ret, routetables...)

	}
	return ret, nil
}

func (self *SRegion) CreatePeeringRoute(vpcId, destinationCidr, target string) error {
	params := jsonutils.NewDict()
	routeObj := jsonutils.NewDict()
	routeObj.Set("type", jsonutils.NewString("peering"))
	routeObj.Set("nexthop", jsonutils.NewString(target))
	routeObj.Set("destination", jsonutils.NewString(destinationCidr))
	routeObj.Set("vpc_id", jsonutils.NewString(vpcId))
	params.Set("route", routeObj)
	err := DoCreate(self.ecsClient.VpcRoutes.Create, params, nil)
	if err != nil {
		return errors.Wrapf(err, "DoCreate(self.ecsClient.VpcRoutes.Create, %s, &ret)", jsonutils.Marshal(params).String())
	}
	return nil
}

func (self *SRegion) DeletePeeringRoute(routeId string) error {
	err := DoDelete(self.ecsClient.VpcRoutes.Delete, routeId, nil, nil)
	if err != nil {
		return errors.Wrapf(err, "DoDelete(self.ecsClient.VpcRoutes.Delete,%s,nil)", routeId)
	}
	return nil
}
